////////////////////////////////////////////////////////////////////////////////
//
//  Copyright (C) 2003-2006 Adobe Macromedia Software LLC and its licensors.
//  All Rights Reserved. The following is Source Code and is subject to all
//  restrictions on such code as contained in the End User License Agreement
//  accompanying this product.
//
////////////////////////////////////////////////////////////////////////////////

package mx.charts.chartClasses
{

import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.DisplayObject;
import flash.display.DisplayObjectContainer;
import flash.display.Graphics;
import flash.display.Shape;
import flash.events.Event;
import flash.events.IEventDispatcher;
import flash.events.MouseEvent;
import flash.geom.Matrix;
import flash.geom.Point;
import flash.geom.Rectangle;
import flash.utils.*;
import mx.charts.ChartItem;
import mx.charts.HitData;
import mx.charts.events.ChartItemEvent;
import mx.charts.styles.HaloDefaults;
import mx.collections.ArrayCollection;
import mx.collections.ICollectionView;
import mx.collections.IList;
import mx.collections.ListCollectionView;
import mx.collections.XMLListCollection;
import mx.core.FlexShape;
import mx.core.IDataRenderer;
import mx.core.IFlexDisplayObject;
import mx.core.UIComponent;
import mx.core.mx_internal;
import mx.effects.Parallel;
import mx.effects.effectClasses.ParallelInstance;
import mx.events.EffectEvent;
import mx.graphics.IFill;
import mx.graphics.IStroke;
import mx.graphics.Stroke;
import mx.graphics.SolidColor;
import mx.managers.IFocusManagerComponent;
import mx.managers.ILayoutManagerClient;
import mx.styles.CSSStyleDeclaration;
import mx.styles.ISimpleStyleClient;

use namespace mx_internal;

//--------------------------------------
//  Events
//--------------------------------------

/**
 *  Dispatched when a data point is found under the mouse pointer
 *  when it is clicked.
 *  Flex considers only data points within the radius determined by
 *  the <code>mouseSensitivity</code> property.
 *
 *  @eventType mx.charts.events.ChartItemEvent.MOUSE_CLICK_DATA
 */
[Event(name="itemClick", type="mx.charts.events.ChartItemEvent")]

/**
 *  Dispatched when a data point is found under the mouse pointer
 *  when it is double clicked.
 *  Flex considers only data points within the radius determined by
 *  the <code>mouseSensitivity</code> property.
 *
 *  @eventType mx.charts.events.ChartItemEvent.ITEM_DOUBLE_CLICK
 */
[Event(name="itemDoubleClick", type="mx.charts.events.ChartItemEvent")]

/**
 *  Dispatched when a data point is found under the mouse pointer
 *  when it is pressed down.
 *  Flex considers only data points within the radius determined by
 *  the <code>mouseSensitivity</code> property.
 *
 *  @eventType mx.charts.events.ChartItemEvent.ITEM_MOUSE_DOWN
 */
[Event(name="itemMouseDown", type="mx.charts.events.ChartItemEvent")]

/**
 *  Dispatched when a data point is found under the mouse pointer
 *  when it is released.
 *  Flex considers only data points within the radius determined by
 *  the <code>mouseSensitivity</code> property.
 *
 *  @eventType mx.charts.events.ChartItemEvent.ITEM_MOUSE_UP
 */
[Event(name="itemMouseUp", type="mx.charts.events.ChartItemEvent")]

/**
 *  Dispatched when a new data point is found under the mouse pointer.
 *  Flex only considers data points within the radius determined by
 *  the <code>mouseSensitivity</code> property.
 *
 *  @eventType mx.charts.events.ChartItemEvent.ITEM_ROLL_OVER
 */
[Event(name="itemRollOver", type="mx.charts.events.ChartItemEvent")]

/**
 *  Dispatched when the closest data point under the mouse pointer changes.
 *  Flex considers only data points within the radius determined by
 *  the <code>mouseSensitivity</code> property.
 *
 *  @eventType mx.charts.events.ChartItemEvent.ITEM_ROLL_OUT
 */
[Event(name="itemRollOut", type="mx.charts.events.ChartItemEvent")]

/**
 *  Dispatched when the mouse pointer moves while over a data point.
 *  Flex considers only data points within the radius determined by
 *  the <code>mouseSensitivity</code> property.
 *
 *  @eventType mx.charts.events.ChartItemEvent.ITEM_MOUSE_MOVE
 */
[Event(name="itemMouseMove", type="mx.charts.events.ChartItemEvent")]

/**
 *  @private
 *  Hidden from metadata, since it's not to exposed to end users
 */
/*
[Event(name="legendDataChanged", type="flash.events.Event")]
*/

//--------------------------------------
//  Styles
//--------------------------------------

include "../../styles/metadata/PaddingStyles.as"

/**
 *  Contains a list of Strings, each corresponding to a CSS type selector
 *  to be used as the default CSS style for a series.
 *  Each series in the chart draws a CSS type selector from the list
 *  contained in the <code>chartSeriesStyles</code> style.
 *  The first series uses the first selector, the second uses
 *  the second, and so on.
 *  Style properties in this type selector can be overridden
 *  by specifying a <code>styleName</code> property for the series.
 */
[Style(name="chartSeriesStyles", type="Array", arrayType="String", inherit="no")]

/**
 *  Color of text in the component, including the component label.
 *  
 *  @default 0x0B333C
 */
[Style(name="color", type="uint", format="Color", inherit="yes")]

/**
 *  The Stroke to use to render the callout line
 *  from the data tip target to the tip.
 *  If set to <code>null</code>, no Stroke is drawn
 */
[Style(name="dataTipCalloutStroke", type="mx.graphics.IStroke", inherit="no")]

/**
 *  Specifies the class to use to render data tips.
 *  This class must implement the IFlexDisplayObject
 *  and IDataRenderer interfaces.
 *  The chart assigns the custom data tip's <code>data</code> property
 *  to be the HitData structure describing the data point being described.
 */
[Style(name="dataTipRenderer", type="Class", inherit="no")]

/**
 *  Specifies the fill style used for the chart background.
 *  The fill can be either a simple color value
 *  or an object that implements the mx.graphics.IFill interface.
 */
[Style(name="fill", type="mx.graphics.IFill", inherit="no")]

/**
 *  Name of the font to use.
 *  Unlike in a full CSS implementation,
 *  comma-separated lists are not supported.
 *  You can use any font family name.
 *  If you specify a generic font name,
 *  it is converted to an appropriate device font.
 *  
 *  @default "Verdana"
 */
[Style(name="fontFamily", type="String", inherit="yes")]

/**
 *  Height of the text in pixels.
 *  
 *  @default 10
 */
[Style(name="fontSize", type="Number", format="Length", inherit="yes")]

/**
 *  Determines whether the text is italic font.
 *  Recognized values are <code>"normal"</code> and <code>"italic"</code>.
 *  
 *  @default "normal"
 */
[Style(name="fontStyle", type="String", enumeration="normal,italic", inherit="yes")]

/**
 *  Determines whether the text is boldface.
 *  Recognized values are <code>"normal"</code> and <code>"bold"</code>.
 *  
 *  @default "normal"
 */
[Style(name="fontWeight", type="String", enumeration="normal,bold", inherit="yes")]

/**
 *  Specifies the maximum number of datatips a chart will show.
 *  If more datapoints are in range of the chart than allowed by this style,
 *  they will be prioritized by distance,
 *  with the closest showing first.
 *  
 *  <p>The default value is NaN, which shows whatever datatips are within range.</p>
 */
[Style(name="maximumDataTipCount", type="int", inherit="no")]

/**
 *  Specifies the number of pixels between the chart's bottom border
 *  and its content area.
 */
[Style(name="paddingBottom", type="Number", format="Length", inherit="no")]

/**
 *  Specifies the number of pixels between the chart's top border
 *  and its content area.
 */
[Style(name="paddingTop", type="Number", format="Length", inherit="no")]

/**
 *  Specifies whether to show targets over the datapoints
 *  when <code>showDataTips</code> is set to true.
 */
[Style(name="showDataTipTargets", type="Boolean", inherit="no")]

/**
 *  Determines whether the text is underlined.
 *  Possible values are <code>"none"</code> and <code>"underline"</code>.
 *  
 *  @default "none"
 */
[Style(name="textDecoration", type="String", enumeration="none,underline", inherit="no")]

//--------------------------------------
//  Other metadata
//--------------------------------------

[DefaultProperty("dataProvider")]

/**
 *  @private
 *
 *  Please read the terms of the Charts EULA.
 */
[RequiresLicense(id="mx.charts", handler="mx.charts.chartClasses.ChartsLicenseHandler")]

/**
 *  The ChartBase class is the link between the Flex component
 *  architecture and the DualStyleObject architecture.
 *  It extends the Flex base class UIComponent,
 *  but contains DualStyleObject classes.
 *
 *  <p>You typically do not use the ChartBase class directly.
 *  Instead you use one of its subclasses such as PlotChart or BubbleChart.
 *  It acts as the base class for the common chart types
 *  provided in the mx.charts package.</p>
 *  
 *  <p>This class defines a number of CSS styles and properties
 *  that provide easy access to the more common features of the framework.</p>
 *  
 *  <p>A chart's minimum size is 20 x 20 pixels.
 *  A chart's maximum size is unbounded.
 *  A chart's preferred size is 400 x 400 pixels.</p>
 *  
 *  <p>ChartBase objects, and its subclasses, augment the normal
 *  mouse event objects with additional data describing
 *  the nearest chart data point under the mouse.
 *  The event object for mouse events contains an additional property,
 *  <code>hitData</code>, which contains a HitData object
 *  describing the nearest data point.
 *  See mx.charts.HitData for more information on the contents
 *  of the HitData object.</p>
 *  
 *  <p>ChartBase objects only consider data points within a certain radius
 *  of the mouse pointer.
 *  You can set this radius using the <code>mouseSensitivity</code>
 *  property.
 *  If there is no data point within the <code>mouseSensitivity</code>
 *  radius, event.hitData is <code>null</code>.</p>
 *
 *  @mxml
 *  
 *  <p>Flex components inherit the following properties
 *  from the ChartBase class:</p>
 *
 *  <pre>
 *  &lt;mx:<i>tagname</i>
 *    <strong>Properties</strong>
 *    allElements="<i>Array; No default</i>"
 *    annotationElements="<i>Array; No default</i>"
 *    backgroundElements="<i>Array; No default</i>"
 *    chartState="<i></i>"
 *    clipContent="false|true"
 *    dataProvider="<i>No default</i>"
 *    dataRegion"<i>No default</i>"
 *    dataTipFunction="<i>No default</i>"
 *    dataTipLayerIndex"<i>No default</i>"
 *    dataTipMode"<i>No default</i>"
 *    description"<i>No default</i>"
 *    labelElements"<i>No default</i>"
 *    legendData"<i>No default</i>"
 *    mouseSensitivity="5"
 *    series="<i>No default</i>"
 *    seriesFilters"<i>No default</i>"
 *    showDataTips="true|false"
 *    transforms"<i>No default</i>"
 *    
 *    <strong>Styles</strong>
 *    chartSeriesStyles="<i>Style; No default</i>"
 *    dataTipCalloutStroke="<i>Stroke; No default</i>"
 *    dataTipRenderer="<i>Renderer; No default</i>"
 *    fill="<i>IFill; No default</i>"
 *    fontFamily="<i>Verdana</i>"
 *    fontSize="<i>10</i>"
 *    maximumDataTipCount="NaN"
 *    paddingBottom="<i>No default</i>"
 *    paddingTop="<i>No default</i>"
 *    paddingLeft="<i>0</i>"
 *    paddingRight="<i>0</i>"
 *    showDataTipTargets="true|false"  
 *    
 *   <strong>Events</strong>
 *    itemClick="<i>Event; No default</i>"
 *    itemDoubleClick="<i>Event; No default</i>"
 *    itemMouseDown="<i>Event; No default</i>"
 *    itemMouseMove="<i>Event; No default</i>"
 *    itemMouseUp="<i>Event; No default</i>"
 *    itemRollOut="<i>Event; No default</i>"
 *    itemRollOver="<i>Event; No default</i>"
 *  &gt;
 *  </pre>
 *  
 *  @see mx.charts.HitData
 *  @see mx.charts.CategoryAxis
 *  @see mx.charts.LinearAxis
 */
public class ChartBase extends UIComponent implements IFocusManagerComponent
{
	include "../../core/Version.as";

	//--------------------------------------------------------------------------
	//
	//  Class Initialization
	//
	//--------------------------------------------------------------------------

	/**
	 *  @private
	 */
	private static var stylesInited:Boolean = initStyles();
	
	/**
	 *  @private
	 */
	private static function initStyles():Boolean
	{
		HaloDefaults.init();

		var chartBaseFill:IFill = IFill(new SolidColor(0xFFFFFF, 0));

		var selector:CSSStyleDeclaration =
			HaloDefaults.createSelector("ChartBase");

		selector.defaultFactory = function():void
		{
			this.chartSeriesStyles = HaloDefaults.chartBaseChartSeriesStyles;
			this.fill = chartBaseFill;
			this.calloutStroke = new Stroke(0x888888,2);			
			this.dataTipRenderer = DataTip;
			this.fontFamily = "Arial"
			this.fontSize = 10;
		}

		return true;
	}

	//--------------------------------------------------------------------------
	//
	//  Class constants
	//
	//--------------------------------------------------------------------------

	/**
	 *  @private
	 */
	private static const TOOLTIP_TARGET_INNER_RADIUS:Number = 1.5;
	
	/**
	 *  @private
	 */
	private static const TOOLTIP_TARGET_RADIUS:Number = 4.5;

	/**
	 *  @private
	 */
	private static var ITEM_EVENTS:Object =
	{
		itemClick: true,
		itemDoubleClick: true,
		itemMouseDown: true,
		itemMouseUp: true,
		itemMouseMove: true,
		itemRollOver: true,
		itemRollOut: true
	}

	//--------------------------------------------------------------------------
	//
	//  Constructor
	//
	//--------------------------------------------------------------------------

	/**
	 *  Constructor.
	 */
	public function ChartBase()
	{
		super();

        try
        {
    		var licenseHandlerClass:Class = Class(getDefinitionByName(
				"mx.charts.chartClasses.ChartsLicenseHandler"));
    		if (licenseHandlerClass != null)
    		    licenseHandlerClass["displayWatermark"](this);
        }
        catch(e:Error)
        {
		}
		
		tabEnabled = false;

		_seriesHolder = new UIComponent();
		_seriesFilterer = new UIComponent();
		_seriesHolder.addChild(_seriesFilterer);
		addChild(_seriesHolder);

		_backgroundElementHolder = new UIComponent();
		addChild(_backgroundElementHolder);

		_annotationElementHolder = new UIComponent();
		addChild(_annotationElementHolder);
		
		_dataTipOverlay = new FlexShape();
		_dataTipOverlay.name = "dataTipOverlay";
		addChild(_dataTipOverlay);

		var g:Graphics;
		
		_seriesMask = new FlexShape();
		_seriesMask.name = "seriesMask";
		g = _seriesMask.graphics;
		g.clear();
		g.beginFill(0, 100);
		g.drawRect(0,0,10,10);
		g.endFill();	
		_seriesMask.visible = false;

		_backgroundElementMask = new FlexShape();
		_backgroundElementMask.name = "backgroundElementMask";
		g = _backgroundElementMask.graphics;
		g.clear();
		g.beginFill(0, 100);
		g.drawRect(0,0,10,10);
		g.endFill();	
		_backgroundElementMask.visible = false;

		_annotationElementMask = new FlexShape();
		_annotationElementMask.name = "annotationElementMask";
		g = _annotationElementMask.graphics;
		g.clear();
		g.beginFill(0, 100);
		g.drawRect(0,0,10,10);
		g.endFill();	
		_annotationElementMask.visible = false;
		
		_seriesHolder.addChild(_seriesMask);
		_seriesHolder.mask = _seriesMask;		

		_backgroundElementHolder.addChild(_backgroundElementMask);
		_backgroundElementHolder.mask = _backgroundElementMask;

		_annotationElementHolder.addChild(_annotationElementMask);
		_annotationElementHolder.mask = _annotationElementMask;
		
		_currentHitSet = [];
				
		invalidateChildOrder();
	}

	//--------------------------------------------------------------------------
	//
	//  Variables
	//
	//--------------------------------------------------------------------------

	/**
	 *  @private
	 */
	private var _tipCache:InstanceCache;

	/**
	 *  @private
	 */
	private var _dataTipOverlay:Shape;
	
	/**
	 *  We need commitProperties() to be called before an update.
	 *  Since there are scenarios where that might not be guaranteed,
	 *  we'll enforce it ourselves.
	 */
	private var _commitPropertiesCalled:Boolean = false;
	
	/**
	 *  @private
	 */
	private var _seriesMask:Shape;

	/**
	 *  @private
	 */
	private var _backgroundElementMask:Shape;

	/**
	 *  @private
	 */
	private var _annotationElementMask:Shape;
	
	/**
	 *  @private
	 */
	private var _bSeriesDirty:Boolean = false;
	
	/**
	 *  @private
	 */
	private var _bDataDirty:Boolean = false;
	
	/**
	 *  @private
	 */
	private var _childOrderDirty:Boolean = false;
	
	/**
	 *  @private
	 */
	private var _userSeries:Array = [];
	
	/**
	 *  @private
	 */
	private var _backgroundElements:Array = [];
	
	/**
	 *  @private
	 */
	private var _annotationElements:Array = [];
	
	/**
	 *  @private
	 */
	private var _description:String = "";
	
	/**
	 *  @private
	 */
	private var _dataProvider:ICollectionView;

	/**
	* @private
	*/
	protected var _seriesHolder:UIComponent;
	
	/**
	* @private
	*/
	protected var _seriesFilterer:UIComponent;

	/**
	* @private
	*/
	protected var _backgroundElementHolder:UIComponent;

	/**
	* @private
	*/
	protected var _annotationElementHolder:UIComponent;
	
	/**
	 *  The set of all chart elements displayed in the chart.
	 *  This includes series, second series, background elements,
	 *  annotation elements, etc.
	 */
	protected var allElements:Array = [];
	
	/**
	 *  The set of DisplayObjects representing the labels
	 *  for the chart elements.
	 *  Some series, annotation, and background types include overlays
	 *  like elements, callouts, etc.
	 *  If an element wants to display these overlays on top of all elements
	 *  in the chart, they can pass a DisplayObject to the chart containing
	 *  the overlays to be placed on top of all other chart elememnts.
	 *  Chart implementors can access these overlay objects
	 *  in the labelElements Array.
	 */
	protected var labelElements:Array = [];
	
	/**
	 *  @private
	 */
	private var _clipContent:Boolean = true;

	/**
	* @private
	*/
	protected var _transforms:Array;

	/**
	 *  @private
	 */
	private var _showDataTips:Boolean = false;

	/**
	 *  @private
	 */
	private var _dataTipMode:String = "multiple";

	/**
	 *  @private
	 */
	private var _currentHitSet:Array;

	/**
	 *  @private
	 */
	private var _mouseEventsInitialzed:Boolean = false;

	/**
	 *  @private
	 */
	private var _transitionState:uint = 0;
	
	/**
	 *  @private
	 */
	private var _transitionEffect:ParallelInstance;

	/**
	 *  @private
	 */
	private var _seriesStylesDirty:Boolean = true;
	
	//-------------------------------------------------------------------------
	//
	//  Properties
	//
	//--------------------------------------------------------------------------

	//----------------------------------
	//  annotationElements
	//----------------------------------

    [Inspectable(arrayType="mx.charts.chartClasses.ChartElement")]
	
	/**
	 *  Sets an Array of ChartElements that appears on top
	 *  of any data series rendered by the chart.
	 *  Each item in the Array must subclass the mx.charts.DualStyleObject
	 *  class and implement the IChartElement interface.
	 */
	public function get annotationElements():Array
	{
		return _annotationElements;
	}
	
	/**
	 *  @private
	 */
	public function set annotationElements(value:Array):void
	{
		_annotationElements = value;

		invalidateSeries();
	}

	//----------------------------------
	//  backgroundElements
	//----------------------------------

    [Inspectable(arrayType="mx.charts.chartClasses.ChartElement")]

	/**
	 *  Sets an Array of background chart elements that appear below
	 *  any data series rendered by the chart.
	 *  Each item in the Array must subclass the mx.charts.DualStyleObject
	 *  class and implement the IChartElement interface.
	 */
	public function get backgroundElements():Array
	{
		return _backgroundElements;
	}
	
	/**
	 *  @private
	 */
	public function set backgroundElements(value:Array):void
	{
		_backgroundElements = value;

		invalidateSeries();
	}

	//----------------------------------
	//  chartState
	//----------------------------------

	[Inspectable(environment="none")]

	/**
	 *  The current transition state of the chart.
	 *  This property can be used to determine whether the chart
	 *  is currently transitioning out its old data,
	 *  transitioning in its new data,
	 *  or has completed all transitions and is showing its current data set.
	 *  See the mx.charts.chartClasses.ChartState enumeration
	 *  for possible values.
	 */
	public function get chartState():uint
	{
		return _transitionState;
	}

	/**
	* @private
	*/
	protected function setChartState(value:uint):void
	{
		if (_transitionState == value)
			return;

		var oldState:uint = _transitionState;
		_transitionState = value;

		var n:int = allElements.length;
		for (var i:int = 0; i < n; i++)
		{
			var g:IChartElement = allElements[i] as IChartElement;
			if (g)
				g.chartStateChanged(oldState, value);
		}

		invalidateDisplayList();
	}

	//----------------------------------
	//  clipContent
	//----------------------------------

	[Inspectable(defaultValue="true")]

	/**
	 *  Determines if Flex clips the chart to the area bounded by the axes.
	 *  Set to <code>false</code> to clip the chart.
	 *  Set to <code>true</code> to avoid clipping when the data is rendered.
	 */
	public function get clipContent():Boolean
	{
		return _clipContent;
	}

	/**
	 *  @private
	 */
	public function set clipContent(value:Boolean):void
	{
		if (_clipContent == value)
			return;
		_clipContent = value;

		_seriesHolder.mask = _clipContent ?
							 _seriesMask :
							 null;

		_backgroundElementHolder.mask = _clipContent ?
										_backgroundElementMask :
										null;

		_annotationElementHolder.mask = _clipContent ?
										_annotationElementMask :
										null;
	}
    
	//----------------------------------
	//  dataProvider
	//----------------------------------
	
	[Inspectable(category="Data", arrayType="Object")]

	/**
	 *  Specifies the data provider for the chart.
	 *  Data series rendered by the chart are assigned this data provider.
	 *  To render disparate data series in the same chart,
	 *  use the <code>dataProvider</code> property on the individual series.
	 *  
	 *  <p>This property can accept an Array
	 *  or any other object that implements the IList or ICollectionView interfaces.</p>
	 */
	public function get dataProvider():Object
	{
		return _dataProvider;
	}

	/**
	 *  @private
	 */
	public function set dataProvider(value:Object):void
	{
		if (value is Array)
		{
			value = new ArrayCollection(value as Array);
		}
		else if (value is ICollectionView)
		{
		}
		else if (value is IList)
		{
			value = new ListCollectionView(value as IList);
		}
		else if (value is XMLList)
		{
			value = new XMLListCollection(XMLList(value));
		}
		else if (value != null)
		{
			value = new ArrayCollection([ value ]);
		}
		else
		{
			value = new ArrayCollection();
		}
		
		_dataProvider = ICollectionView(value);

		invalidateData();
	}

	//----------------------------------
	//  dataRegion
	//----------------------------------

	/**
	 *  The area of the chart used to display data.
	 *  This rectangle excludes the area used for gutters, axis lines and labels, and padding.
	 */
	protected function get dataRegion():Rectangle
	{
		return null;
	}

	//----------------------------------
	//  dataTipFunction
	//----------------------------------

	[Inspectable(category="Data")]
	
	/**
	 *  Specifies a callback method used to generate DataTips from values.
	 *  This method should use the following signature:
	 *  <pre>
	 *  function dataTipFunction(<i>hitData</i>:Hit):String
	 *  </pre>
	 *  <p>This method returns a String that is displayed for each data point.
	 *  The text can include HTML formatting.
	 *  The single parameter is a HitData object that describes the data point.</p>
	 */
	public var dataTipFunction:Function;

	//----------------------------------
	//  dataTipLayerIndex
	//----------------------------------

	/**
	 *  The index of the child that is responsible for rendering DataTips.
	 *  Derived classes that add additional visual elements to the chart
	 *  should add it below this layer.
 	 */
	protected function get dataTipLayerIndex():int
	{
		return getChildIndex(_dataTipOverlay);
	}

	//----------------------------------
	//  dataTipMode
	//----------------------------------
	
	[Inspectable(category="Data", enumeration="single,multiple", defaultValue="single")]

	/**
	 *  Specifies how Flex displays DataTip controls for the chart.
	 *  DataTip controls are similar to ToolTips, except that they display
	 *  an appropriate value representing the nearest chart data point
	 *  under the mouse pointer.
	 *  <p>Different chart elements might show different styles
	 *  of DataTip controls.
	 *  For example, a stacked chart element might show both the values
	 *  of the column and the percentage it contributes to the whole.</p>
	 *
	 *  <p>You can customize DataTip controls with the
	 *  <code>dataTipFunction</code> property. Possible values are:
	 *   <ul>
	 *    <li><code>"single"</code> - dataTips are shown for the closest dataPoint
	 *    to the mouse cursor.</li>
	 *  
	 *    <li><code>multiple</code> - dataTips are shown for any dataTip
	 *    within range of the mouse cursor. You can control the sensitivity 
	 *    with the <code>mouseSensitivity</code> property.</li>
	 *   </ul>
	 *  </p>
	 */
	public function get dataTipMode():String
	{
		return _dataTipMode;
	}
	
	/**
	 *  @private
	 */
	public function set dataTipMode(value:String):void
	{
		_dataTipMode = value;

		updateDataTips();				
	}

	//----------------------------------
	//  description
	//----------------------------------

	[Inspectable(defaultValue="")]

	/**
	 *  A short description of the data described by the chart.
	 *  Screen Readers use this property to describe the chart
	 *  when accessibility is enabled.
	 *  <p>This string defaults to the empty string, and must be
	 *  explicitly assigned by the developer to have meaning.</p>
	 */
	public function get description():String
	{
		return _description;
	}

	/**
	 *  @private
	 */
	public function set description(value:String):void
	{
		setDescription(value);
		tabEnabled = true;
	}
    
	/**
	 *  @private
	 */
	mx_internal function setDescription(value:String):void
	{
		_description = value;
	}

	//----------------------------------
	//  legendData
	//----------------------------------

    [Bindable("legendDataChanged")]
	[Inspectable(environment="none")]
	
	/**
	 *  An Array of Legend items.
	 */
	public function get legendData():Array
	{
		var keyItems:Array = [];
		
		var n:int = allElements.length;
		for (var i:int = 0; i < n; i++)
		{
			var s:Series = allElements[i] as Series;
			if (s)
				keyItems = keyItems.concat(s.legendData);
		}

		return keyItems;
	}

	//----------------------------------
	//  mouseSensitivity
	//----------------------------------

	/**
	 *  Specifies the distance, in pixels, that Flex considers a data point
	 *  to be under the mouse pointer when the mouse moves around a chart.
	 *  Flex considers any data point less than <code>mouseSensitivity</code>
	 *  pixels away to be under the mouse pointer.
	 *  This value is also used by the <code>findDataPoints</code> method.
	 *
	 *  @default 5
	 */
	public var mouseSensitivity:Number = 5;

	//----------------------------------
	//  series
	//----------------------------------

    [Inspectable(category="Data", arrayType="mx.charts.chartClasses.Series")]

	/**
	 *  An Array of Series objects that define the chart data.
	 *  Each chart defines the type of Series objects
	 *  that you use to populate this array.
	 *  For example, a ColumnChart expects ColumnSeries objects
	 *  as part of this Array.
	 *  Some charts accept any object of type IChartElement
	 *  as part of the Array, but in general each chart
	 *  expects a specific type.
	 */
	public function get series():Array
	{
		return _userSeries;
	}

	/**
	 *  @private
	 */
	public function set series(value:Array):void
	{
		value = value == null ? [] : value;
		_userSeries = value;
		
		invalidateSeries();
		invalidateData();
		legendDataChanged();
	}

	//----------------------------------
	//  seriesFilters
	//----------------------------------

	/**
	 *  An Array of filters that are applied to all series in the chart. 
	 *  Assign an Array of Bitmap Filters to this property to apply them to all the series at once. 
	 *  Set the <code>seriesFilter</code> property to an empty Array to clear the default 
	 *  filters on the chart's series.
	 *  Assigning filters to the <code>seriesFilters</code> property, which applies to all 
	 *  series, is more efficient than assigning them on individual series.
	 */
	public function get seriesFilters():Array
	{
		return _seriesFilterer.filters;
	}
	
	/**
	 *  @private
	 */
	public function set seriesFilters(value:Array):void
	{
		_seriesFilterer.filters = value;
	}
	
	//----------------------------------
	//  showDataTips
	//----------------------------------

	[Inspectable(category="Data", defaultValue="false")]

	/**
	 *  Specifies whether Flex shows DataTip controls for the chart.
	 *  DataTip controls are similar to ToolTips,
	 *  except that they display an appropriate value
	 *  representing the nearest chart data point under the mouse pointer.
	 *  
	 *  <p>Different chart elements might show different styles
	 *  of DataTip controls.
	 *  For example, a stacked chart element might show both the values
	 *  of the column and the percentage it contributes to the whole.</p>
	 *
	 *  <p>You can customize DataTip controls with the
	 *  <code>dataTipFunction</code> property.</p>
	 */
	public function get showDataTips():Boolean
	{
		return _showDataTips;
	}

	/**
	 *  @private
	 */
	public function set showDataTips(value:Boolean):void
	{
		if (_showDataTips == value)
			return;
			
		_showDataTips = value;

		if (_showDataTips)
		{
			if (_mouseEventsInitialzed == false)
				setupMouseDispatching();
		}

		updateDataTips();
	}
    
	//----------------------------------
	//  transforms
	//----------------------------------

	/**
	 *  The DataTransforms managed by this chart.
	 *  Each chart contains one or more data transform,
	 *  representing a set of axes used to map from tuples
	 *  from data space to screen space and vice-versa.
	 *  Most charts only have a single DataTransform,
	 *  but CartesianCharts that support second axes
	 *  may manage a second DataTransform as needed.
	 */
	protected function get transforms():Array
	{
		return _transforms;
	}
	
	/**
	 *  @private
	 */
	protected function set transforms(value:Array):void
	{
		_transforms = value;

		invalidateProperties();
		invalidateSeries();
		invalidateData();

		legendDataChanged();
	}

	//--------------------------------------------------------------------------
	//
	//  Inherited methods: EventDispatcher
	//
	//--------------------------------------------------------------------------

	/**
	 *  @private
	 */
	override public function addEventListener(
									type:String, listener:Function,
									useCapture:Boolean = false,
									priority:int = 0,
									useWeakReference:Boolean = false):void
	{
		if (_mouseEventsInitialzed == false &&
			ITEM_EVENTS[type] == true)
		{
			setupMouseDispatching();
		}

		super.addEventListener(type, listener, useCapture,
							   priority, useWeakReference);
	}

	//--------------------------------------------------------------------------
	//
	//  Inherited methods: UIComponent
	//
	//--------------------------------------------------------------------------

	/**
	 *  @private
	 */
	override public function invalidateSize():void
	{
		cancelEffect();

		super.invalidateSize();
	}
	
	/**
	 *  @private
	 */
	override protected function commitProperties():void
	{
		_commitPropertiesCalled = true;

		super.commitProperties();

		if (_bSeriesDirty)
		{
			updateSeries();
			_bSeriesDirty = false;
		}

		updateSeriesStyles();

		if (_bDataDirty)
		{
			updateData();
			_bDataDirty = false;
		}
		
		if (_childOrderDirty)
		{
			updateChildOrder();
			_childOrderDirty = false;
		}
	}

	/**
	 *  @private
	 */
	override protected function measure():void
	{
		super.measure();

		measuredWidth = 400;
		measuredHeight = 400;
	}

	/**
	 *  @private
	 */
	override protected function updateDisplayList(unscaledWidth:Number,
												  unscaledHeight:Number):void
	{
		super.updateDisplayList(unscaledWidth, unscaledHeight);

		// Since there are some scenarios where we might get
		// an updateDisplayList() before a commitProperties(),
		//  we need to guarantee it.
		if (_commitPropertiesCalled == false)
			commitProperties();
			
		graphics.clear();
		graphics.lineStyle(0, 0, 0);

		var fill:IFill = GraphicsUtilities.fillFromStyle(getStyle("fill"));

		GraphicsUtilities.fillRect(graphics, 0, 0,
			unscaledWidth, unscaledHeight, fill);

		advanceEffectState();
	}

    /**
     *  @inheritDoc
     */
    override public function setActualSize(w:Number, h:Number):void
    {
    	if (w != this.width || h != this.height)
	    	cancelEffect();
    	
		super.setActualSize(w, h);
    }

	/**
	 *  @private
	 */
	override public function styleChanged(styleProp:String):void
	{
		if (styleProp == null || styleProp == "chartSeriesStyles")
			invalidateSeries();

		super.styleChanged(styleProp);
	}
	
	//--------------------------------------------------------------------------
	//
	//  Methods
	//
	//--------------------------------------------------------------------------

	/**
	 *  Retrieves the Axis instance for a particular dimension of the chart.
	 *  This is a low level accessor.
	 *  You typically retrieve the Axis directly through a named property
	 *  (such as <code>horizontalAxis</code>, <code>verticalAxis</code>,
	 *  or <code>radiusAxis</code>).
	 */
	public function getAxis(dimension:String):IAxis
	{
		return transforms[0].getAxis(dimension);
	}

	/**
	 *  Assigns an axis instance to a particular dimension of the chart.
	 *  This is a low level accessor.
	 *  You typically set the Axis directly through a named property
	 *  (such as <code>horizontalAxis</code>, <code>verticalAxis</code>,
	 *  or <code>radiusAxis</code>).
	 */
	public function setAxis(dimension:String, value:IAxis):void
	{
		transforms[0].setAxis(dimension, value);
	}

	/**
	 *  Converts a tuple of data values to an x-y coordinate on screen.
	 *  Call this function to transform data on to the screen
	 *  using the same transform that the individual elements go through.
	 *  For example, to create a custom highlight for a data region of a chart,
	 *  you might use this function to determine the on-screen coordinates
	 *  of the range of interest.
	 *  
	 *  <p>For Cartesian charts, you typically pass two values.
	 *  The first value maps to the horizontal axis,
	 *  and the second value maps to the vertical axis.</p>
	 *  
	 *  <p>For polar charts, the first value maps to the angular axis,
	 *  and the second maps to the radial axis.</p>
	 *  
	 *  @return Coordinates that are relative to the chart.
	 */
	public function dataToLocal(... dataValues):Point
	{
		return null;
	}

	/**
	 *  Converts a coordinate on screen to a tuple of data values.
	 *  Call this function to determine what data values
	 *  a particular point on-screen represents.
	 *  <p>Individual chart types determine how this transformation occurs.
	 *  The point should be relative to the chart's coordinate space.</p>
	 *  
	 *  @param pt The Point to convert.
	 *  
	 *  @return The tuple of data values.
	 */
	public function localToData(pt:Point):Array
	{
		return null;
	}

	/**
	 *  Returns an Array of HitData objects that describe
	 *  the nearest data point to the coordinates passed to the method.
	 *  The <code>x</code> and <code>y</code> arguments should be values
	 *  in the ChartBase's coordinate system.
	 *  <p>This method adheres to the limits specified by the
	 *  <code>mouseSensitivity</code> property of the ChartBase object
	 *  when looking for nearby data points.</p>
	 *
	 *  @param x The x coordinate relative to the ChartBase object.
	 *  @param y The y coordinate relative to the ChartBase object.
	 *
	 *  @return An Array of HitData objects.
	 *  
	 *  @see mx.charts.HitData
	 */
	public function findDataPoints(x:Number, y:Number):Array
	{
		var result:Array = [];
		
		if (dataRegion.contains(x, y) == false)
			return result;
			
		var n:int;
		var i:int;
		
		n = allElements.length;
		for (i = n-1; i >= 0; i--)
		{
			var g:IChartElement = allElements[i] as IChartElement;
			
			if (g == null || g.visible == false)
				continue;
			
			var hds:Array = g.findDataPoints(x - _seriesHolder.x,
											 y - _seriesHolder.y,
											 mouseSensitivity);

			if (hds.length == 0)
				continue;
			
			result = result.concat(hds);
		}

		for (i = 0; i < result.length; i++)
		{
			result[i].x += _seriesHolder.x;
			result[i].y += _seriesHolder.y;
		}

		return result;
	}

	/**
	 *  Dispatches a new LegendDataChanged event.
	 */
	public function legendDataChanged():void
	{
		dispatchEvent(new Event("legendDataChanged"));
	}

	/**
	 *  Informs the chart that the underlying data being displayed in the chart 
	 *  has been changed. 
	 *  Chart series and elements call this function when their rendering has 
	 *  changed in order to trigger a coordinated execution of show and hide data effects. 
	 *  You typically do not call this method on the chart directly.
	 */
	public function hideData():void
	{
		if (_transitionEffect != null)
		{
			setChartState(ChartState.NONE);
			_transitionEffect.end();
			_transitionEffect = null;
		}

		setChartState(ChartState.PREPARING_TO_HIDE_DATA);
	}

	/**
	 *  Triggers a redraw of the chart.
	 *  Call this method when you change the style properties
	 *  of the chart's series.
	 */
	public function invalidateSeriesStyles():void
	{
		if (!_seriesStylesDirty)
		{
			_seriesStylesDirty = true;

			invalidateProperties();
		}
	}
	
	/**
	 *  Informs the chart that its series Array has been modified and 
	 *  should be re-processed. Derived chart classes can call this method to trigger a 
	 *  call to its <code>updateSeries()</code> method on the next commitProperties cycle.
	 */
	protected function invalidateSeries():void
	{
		if (_bSeriesDirty == false)
		{
			_bSeriesDirty = true;

			invalidateProperties();
			invalidateDisplayList();
		}
	}
	
	/**
	 *  Informs the chart that its child list has been modified and should be re-ordererd. 
	 *  Derived chart classes can call this method to trigger a call to its 
	 *  <code>updateChildOrder()</code> method on the next <code>commitProperties()</code> cycle.
	 */
	protected function invalidateChildOrder():void
	{
		if (_childOrderDirty == false)
		{
			_childOrderDirty = true;

			invalidateProperties();
		}
	}

	/**
	 *  @private
	 */
	private function cancelEffect():void
	{
		if (_transitionEffect != null)
		{
			_transitionEffect.end();
			_transitionEffect = null;
		}

		setChartState(ChartState.NONE);
	}

	/**
	 *  @private
	 */
	private function advanceEffectState():void
	{
		var p:Parallel;
		var transitionChildren:Array;
		var i:int;

		if (_transitionState == ChartState.PREPARING_TO_HIDE_DATA)
		{
			transitionChildren = collectTransitions();
			
			if (transitionChildren.length == 0)
			{
				setChartState(ChartState.PREPARING_TO_SHOW_DATA);
			}
			else
			{
				setChartState(ChartState.HIDING_DATA);

				p = new Parallel();

				_transitionEffect = ParallelInstance(p.createInstance());

				for (i = 0; i < transitionChildren.length; i++)
				{
					_transitionEffect.addChildSet([transitionChildren[i]]);
				}

				_transitionEffect.addEventListener(EffectEvent.EFFECT_END,
												   dataEffectEndHandler);

				_transitionEffect.play();
			}
		}

		if (_transitionState == ChartState.PREPARING_TO_SHOW_DATA)
		{
			transitionChildren = collectTransitions();
			
			if (transitionChildren.length == 0)
			{
				setChartState(ChartState.NONE);
			}
			else
			{
				setChartState(ChartState.SHOWING_DATA);

				p = new Parallel();

				_transitionEffect = ParallelInstance(p.createInstance());

				for (i = 0; i < transitionChildren.length; i++)
				{
					_transitionEffect.addChildSet([transitionChildren[i]]);
				}

				_transitionEffect.addEventListener(EffectEvent.EFFECT_END,
												   dataEffectEndHandler);

				_transitionEffect.play();

			}
		}
		invalidateDisplayList();
	}

	/**
	 *  @private
	 */
	private function setupMouseDispatching():void
	{
		_mouseEventsInitialzed = true;

		super.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler);
		super.addEventListener(MouseEvent.MOUSE_OVER, mouseOverHandler);
		super.addEventListener(MouseEvent.MOUSE_OUT, mouseOutHandler);
		super.addEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler);
		super.addEventListener(MouseEvent.CLICK, mouseClickHandler);
		super.addEventListener(MouseEvent.DOUBLE_CLICK, mouseDoubleClickHandler);
	}

	/**
	 *  @private
	 */
	private function processRollEvents(hitSet:Array, event:MouseEvent):void
	{
		var removedSet:Array = [];
		var addedSet:Array = [];
		var unchangedSet:Array = [];
		var newLen:int = hitSet.length;
		var oldLen:int = _currentHitSet.length;
		var oldIndex:int = 0;
		var newIndex:int = 0;

		if (hitSet.length == 0 && _currentHitSet.length == 0)
			return;

		
		var sortingSet:Array = hitSet.concat();
		if(newLen > 1)
			sortingSet.sortOn("id", Array.NUMERIC);
					
		while (oldIndex < oldLen || newIndex < newLen)
		{
			if (newIndex == newLen ||
				(oldIndex < oldLen &&
				 _currentHitSet[oldIndex].id < sortingSet[newIndex].id))
			{
				removedSet.push(_currentHitSet[oldIndex]);
				oldIndex++;
			} 
			else if (oldIndex == oldLen ||
					 (newIndex < newLen &&
					  _currentHitSet[oldIndex].id > sortingSet[newIndex].id))
			{
				addedSet.push(sortingSet[newIndex]);
				newIndex++;
			}
			else
			{
				unchangedSet.push(sortingSet[newIndex]);
				newIndex++;
				oldIndex++;
			}
		}
		
		if (removedSet.length > 0)
		{
			dispatchEvent(new ChartItemEvent(ChartItemEvent.ITEM_ROLL_OUT,
											 removedSet, event, this));
		}
		
		if (addedSet.length > 0)
		{
			dispatchEvent(new ChartItemEvent(ChartItemEvent.ITEM_ROLL_OVER,
											 addedSet, event, this));
		}
		
		if (unchangedSet.length > 0)
		{
			dispatchEvent(new ChartItemEvent(ChartItemEvent.ITEM_MOUSE_MOVE,
											 unchangedSet, event, this));
		}

		_currentHitSet = hitSet;

		updateDataTipToMatchHitSet(_currentHitSet);
	}

	/**
	 *  @private
	 */
	private function updateDataTips():void
	{
		var hitPoints:Array = findDataPoints(mouseX, mouseY);
		_currentHitSet = hitPoints;

		updateDataTipToMatchHitSet(_currentHitSet);
	}

	/**
	 *  @private
	 */
	private function updateDataTipToMatchHitSet(hitSet:Array):void
	{
		var tipCount:Number;
		
		if (_showDataTips == false)
		{
			tipCount = 0;
		}
		else if (_dataTipMode == "multiple")	
		{
			tipCount= getStyle("maximumDataTipCount");
			if (isNaN(tipCount))
				tipCount = hitSet.length;	
			else
				tipCount = Math.min(tipCount, hitSet.length);
		}
		else // "single"
		{
			tipCount = Math.min(hitSet.length, 1);
		}
		
		if (tipCount == 0)
		{
			if (_tipCache)
				_tipCache.count = 0;
		}
		else
		{
			if (_tipCache == null)
			{
				var tipClass:* = getStyle("dataTipRenderer");
				_tipCache = new InstanceCache(tipClass,
											  systemManager.toolTipChildren);
				_tipCache.creationCallback = tipCreated;

			}
			_tipCache.count = tipCount;

			for (var i:int = 0; i < tipCount; i++)
			{
				var currentHitData:HitData = hitSet[i];
				
				var tipInstance:IFlexDisplayObject  = _tipCache.instances[i];
				
				if (dataTipFunction != null)
					currentHitData.dataTipFunction = invokeDTFunction;
	
				if (tipInstance is IDataRenderer)
					(tipInstance as IDataRenderer).data = currentHitData;
			}
		}

		positionDataTips();
	}
	
	/**
	 *  @private
	 */
	private function tipCreated(tip:IEventDispatcher, cache:InstanceCache):void
	{
		tip.addEventListener(MouseEvent.MOUSE_OVER, mouseOverHandler);
		tip.addEventListener(MouseEvent.MOUSE_OUT, mouseOutHandler);
		tip.addEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler);
	}

	/**
	 *  @private
	 */
	private function invokeDTFunction(hd:HitData):String
	{
		return dataTipFunction(hd);
	}

	/**
	 *  Defines the locations of DataTips on the chart.
	 *  This method ensures that DataTips do not overlap each other
	 *  (if multiple DataTips are visible) or their target data items.
 	 */
	protected function positionDataTips():void
	{
		var tipCount:int = _tipCache != null ? _tipCache.count:0;
		var rc:Rectangle = dataRegion;
		var g:Graphics = _dataTipOverlay.graphics;
		var pData:Array;
		var pLeft:Array = [];
		var pRight:Array = [];
		var sortList:Array = [];
		var i:int;
		var data:TipPositionData;
		var minX:Number;
		var maxX:Number;
		var len:int;
		var stacks:Array;
		var currentStack:TipStack;
		var calloutStroke:IStroke = getStyle("calloutStroke");
		var dataTipHOffset:Number = getStyle("horizontalDataTipOffset");
		var dataTipVOffset:Number = getStyle("horizontalDataTipOffset");
		
		if (isNaN(dataTipHOffset))
			dataTipHOffset = 6;
		if (isNaN(dataTipVOffset))
			dataTipVOffset = 6;
		
		g.clear();

		if (tipCount == 0)
			return;
			
		var showTargetValue:Object = getStyle("showDataTipTargets");
		var showTarget:Boolean = showTargetValue != false &&
								 showTargetValue != "false";
		
		pData = _tipCache.instances.slice(0, tipCount);
		var localPts:Point = new Point();

		for (i = 0; i < tipCount; i++)
		{
			var hitData:HitData = _currentHitSet[i];
			var tipInstance:IFlexDisplayObject = pData[i];

			localPts.x = Math.max(rc.left, Math.min(hitData.x, rc.right));
			localPts.y = Math.max(rc.top, Math.min(hitData.y, rc.bottom));
			var pts:Point = localToGlobal(localPts);
			
			var xpos:Number;
			var ypos:Number;

			if (tipInstance is ILayoutManagerClient )
				ILayoutManagerClient (tipInstance).validateSize();
			
			data = new TipPositionData(tipInstance, hitData,
									   localPts.x, localPts.y, pts.x, pts.y);
						
			if (data.gy - dataTipVOffset - tipInstance.measuredHeight > 0)
				data.py = data.gy - dataTipVOffset - tipInstance.measuredHeight;
			else 						
				data.py = data.gy + dataTipVOffset;

			sortList.push(data);
			pData[i] = data;
		}
		
		if (sortList.length > 0)
		{
			sortList.sortOn("gx", Array.NUMERIC);
			
			minX = sortList[0].gx;
			maxX = sortList[sortList.length - 1].gx;
			
			var screenTL:Point = new Point(4, 4);
			var screenBR:Point = new Point(screen.width - 4, screen.height - 4);
			
			i = 0;

			// This code pre-examines the datatips for tips that would
			// stick out off screen, and forces them to flip to the other side.
			// While this gets datatips that don't stick off screen and don't
			// overlap the points, it makes for some fantastically unattractive
			// datatip layouts.
			// So for now I'm opting for the prettier but occasionally slightly
			// annoying behavior of sliding tips over until they don't stick
			// off screen, potentially overlapping the points.
			/*			
			while (i < sortList.length)
			{
				data = sortList[i];			
				if (maxX + dataTipHOffset + tipInstance.measuredWidth >
					screenBR.x) 
				{
					pLeft.push(data);
					sortList.splice(i, 1);
				}
				else if (minX - dataTipHOffset - tipInstance.measuredWidth <
						 screenTL.x) 
				{
					pRight.push(data);
					sortList.splice(i ,1);
				}
				else
				{
					i++;
				}
			}
			*/
			
			var splitPoint:int = Math.floor(sortList.length / 2);

			// As a first approximation, we just divide these in half.
			// But our goal is to end up with an even set of left/right stacks.
			// So if we already have items in those stacks,
			// let's adjust our split.
			if (pLeft.length > pRight.length)
			{
				splitPoint = Math.max(0, splitPoint -
									  (pLeft.length - pRight.length));
			}
			else if (pRight.length > pLeft.length)
			{
				splitPoint = Math.min(sortList.length,
									  splitPoint +
									  (pRight.length - pLeft.length));
			}

			pLeft = pLeft.concat(sortList.slice(0, splitPoint));
			pRight = pRight.concat(sortList.slice(splitPoint, sortList.length));
		}
				
		pRight.sortOn("gy");
		len = pRight.length;
		stacks = [];
		currentStack = null;

		for (i = 0; i < len; i++)
		{			
			data = pRight[i];
			data.isRight = true;
			data.px = data.gx + dataTipHOffset;
			if (currentStack != null)
			{
				if (data.py < currentStack.gy + currentStack.height)
				{
					currentStack.addTip(data, screen.height);
				}
				else
				{
					reduceStacks(stacks, currentStack);
					currentStack = new TipStack();
					currentStack.addTip(data, screen.height);
				}
			}
			else
			{
				currentStack = new TipStack();
				currentStack.addTip(data, screen.height);
			}
		}

		if (currentStack)
			reduceStacks(stacks, currentStack);
		
		// Now make sure none of the tips block any targets.
		var startTarget:Number = 0;
		var endTarget:Number = 0;
		
		len = stacks.length;
		for (i = 0; i < len; i++)
		{
			stacks[i].positionY();
		}

		pLeft.sortOn("gy");
		len = pLeft.length;
		stacks = [];
		currentStack = null;
		for (i = 0; i < len; i++)
		{			
			data = pLeft[i];
			data.isRight = false;
			data.px = data.gx - data.width - dataTipHOffset;
			
			if (currentStack != null)
			{
				if (data.py < currentStack.gy + currentStack.height)
				{
					currentStack.addTip(data, screen.height);
				}
				else
				{
					reduceStacks(stacks, currentStack);
					currentStack = new TipStack();
					currentStack.addTip(data, screen.height);
				}
			}
			else
			{
				currentStack = new TipStack();
				currentStack.addTip(data, screen.height);
			}
		}

		if (currentStack)
			reduceStacks(stacks, currentStack);

		len = stacks.length;
		for (i = 0; i < len; i++)
		{
			stacks[i].positionY();
		}

		pData.sortOn("py");
		len = pData.length;
		maxX = -Infinity;
		minX = Infinity;

		var minForRightTips:Number = -Infinity;
		var maxForLeftTips:Number = Infinity;
		var repeatLayout:Boolean = true;
		
		// We should never have to go through this loop more than twice.
		var repeatCount:Number = 0;
		while (repeatLayout && repeatCount <= 2)
		{
			repeatLayout = false;
			repeatCount++;
			for (i = 0; i < len; i++)
			{
				var tipData:TipPositionData = pData[i];
				
				var bResetMaxX:Boolean = false;
				var bResetMinX:Boolean = false;
				
				while (startTarget < pData.length &&
					   pData[startTarget].gy + TOOLTIP_TARGET_RADIUS <
					   tipData.py)
				{
					if (pData[startTarget].gx >= maxX)
						bResetMaxX = true;
					if (pData[startTarget].gx <= minX)
						bResetMinX = true;

					startTarget++;
				}

				endTarget = Math.max(endTarget, startTarget);
				
				while (endTarget < pData.length &&
					   pData[endTarget].gy - TOOLTIP_TARGET_RADIUS <
					   tipData.py + tipData.height)
				{
					if (pData[endTarget].gx >= maxX)
					{
						bResetMaxX = false;
						maxX = pData[endTarget].gx;
					}

					if (pData[endTarget].gx <= minX)
					{
						bResetMinX = false;
						minX = pData[endTarget].gx;
					}

					endTarget++;
				}

				if (bResetMaxX || bResetMinX)
				{
					maxX = -Infinity;
					minX = Infinity;
					
					for (var j:int = startTarget; j < endTarget; j++)
					{
						maxX = Math.max(maxX, pData[j].gx);
						minX = Math.min(minX, pData[j].gx);
					}
				}

				if (tipData.isRight)
				{
					tipData.px = Math.max(tipData.px,
										  maxX + TOOLTIP_TARGET_RADIUS);
					tipData.px = Math.max(minForRightTips, tipData.px);
					
					if (tipData.px > screenBR.x - tipData.width)
					{
						tipData.px = screenBR.x - tipData.width;
						if (maxForLeftTips > tipData.px)
						{
							maxForLeftTips = tipData.px;
							repeatLayout = true;
						}
					}
				}
				else
				{
					tipData.px = Math.min(tipData.px,
										  minX - TOOLTIP_TARGET_RADIUS -
										  tipData.width);
					tipData.px = Math.min(maxForLeftTips - tipData.width,
										  tipData.px);
					
					if (tipData.px < screenTL.x)
					{
						tipData.px = screenTL.x;
						if (tipData.px + tipData.width > minForRightTips)
						{
							minForRightTips = tipData.px + tipData.width;
							repeatLayout = true;
						}
					}
				}
				
				tipData.tip.move(tipData.px, tipData.py);

				if (showTarget)
				{	
					if (len > 1)
					{
						if (calloutStroke != null)
						{
							calloutStroke.apply(g);
							pts.x = tipData.px;
							pts.y = tipData.py;
							localPts = globalToLocal(pts);

							if (tipData.isRight)
							{					
								g.moveTo(localPts.x,
										 localPts.y + tipData.height /  2);
								g.lineTo(tipData.x,
										 localPts.y + tipData.height / 2);
								g.lineTo(tipData.x, tipData.y);
							}
							else
							{
								g.moveTo(localPts.x + tipData.width,
										 localPts.y + tipData.height / 2);
								g.lineTo(tipData.x,
										 localPts.y + tipData.height / 2);
								g.lineTo(tipData.x, tipData.y);
							}
						}
					}

					var tipColor:uint = hitData.contextColor;
					g.lineStyle(1, tipColor, 100);
					g.moveTo(tipData.x, tipData.y);
					g.beginFill(0xFFFFFF, 1);
					g.drawCircle(tipData.x, tipData.y, TOOLTIP_TARGET_RADIUS);
					g.endFill();

					g.beginFill(tipColor, 1);
					g.drawCircle(tipData.x, tipData.y,
								 TOOLTIP_TARGET_INNER_RADIUS);
					g.endFill();
				}
			}
		}
	}

	/**
	 *  @private
	 */
	private function reduceStacks(stacks:Array, currentStack:TipStack):void
	{
		while (stacks.length > 0)
		{
			var prevStack:TipStack = stacks[stacks.length - 1];
			if (prevStack.gy + prevStack.height < currentStack.gy)
				break;
			
			prevStack.merge(currentStack, screen.height);
			
			currentStack = stacks.pop();
		}

		stacks.push(currentStack);
	}
	
	/**
	 *  Applies per-series customization and formatting to the series of the chart. 
	 *  This method is called once for each series when the series 
	 *  have been changed by a call to the <code>invalidateSeries()</code> method.
	 *  
	 *	@param	seriesGlyph	The series to customize.
	 *	@param	i The series' index in the series Array.
	 */
	protected function customizeSeries(seriesGlyph:Series, i:uint):void
	{
	}

	/**
	 *  This method triggers a redraw of the chart.
	 *  Call this method when you add or change
	 *  the chart's series or data providers.
	 */
	protected function invalidateData():void
	{
		_bDataDirty=true;
		invalidateProperties();
	}

	/**
	 *  Preprocesses the series and transform for display. You typically don't call 
	 *  this method directly. Instead, this function is called automatically during the 
	 *  chart's <code>commitProperties()</code> cycle when the series have 
	 *  been invalidated by a call to <code>invalidateSeries()</code>.
	 *  <p>By default, this method calls the <code>customizeSeries()</code> method once for each series in the set.  
	 *  Chart subclasses can override this function to add customization logic global to the whole series set.</p>
	 *  
	 *  @param seriesSet An Array of series to preprocess.
	 *  @param transform The transform used by the series.
	 *  
	 *  @return An Array of series with the series set applied.
	 */
	protected function applySeriesSet(seriesSet:Array,
									  transform:DataTransform):Array
	{
		for (var i:int = 0; i < seriesSet.length; i++)
		{
			var newSeries:IChartElement = seriesSet[i];
			if (newSeries is Series)
				customizeSeries(Series(seriesSet[i]), i);
		}

		return seriesSet;
	}

	/**
	 *  @private
	 */
	mx_internal function updateChildOrder():void
	{
		var nextindex:int = 0;
		setChildIndex(_backgroundElementHolder, nextindex++);
		nextindex = updateAxisOrder(nextindex);
		setChildIndex(_seriesHolder, nextindex++);
		setChildIndex(_annotationElementHolder, nextindex++);
		setChildIndex(_dataTipOverlay, nextindex++);
	}

	/**
	 *  @private
	 */
	mx_internal function updateAxisOrder(nextindex:int):int
	{
		return nextindex;
	}
	
	/**
	 *  @private
	 */
	mx_internal function updateSeries():void
	{
		var displayedSeries:Array = applySeriesSet(_userSeries, _transforms[0]);

		var i:int;
		var len:int = displayedSeries ? displayedSeries.length : 0;
		var c:DisplayObject;
		var g:IChartElement;
		var labelLayer:UIComponent;

		len = labelElements.numChildren;

		removeElements(_backgroundElementHolder, true);
		removeElements(_seriesFilterer, false);
		removeElements(_annotationElementHolder, true);

		addElements(_backgroundElements, _transforms[0],
					_backgroundElementHolder);

		allElements = _backgroundElements.concat();

		addElements(displayedSeries, _transforms[0], _seriesFilterer);

		allElements = allElements.concat(displayedSeries);
		
		labelElements = [];
		for (i = 0; i < displayedSeries.length; i++) 
		{
			g = displayedSeries[i] as IChartElement;
			if (!g)
				continue;

			labelLayer = UIComponent(g.labelContainer);
			if (labelLayer) 
				labelElements.push(labelLayer);				
		}

		addElements(labelElements, _transforms[0],_annotationElementHolder);
		
		allElements = allElements.concat(labelElements);

		addElements(_annotationElements,
					_transforms[0],_annotationElementHolder);
		
		allElements = allElements.concat(_annotationElements);

		_transforms[0].elements = _annotationElements.concat(displayedSeries).
								  concat(_backgroundElements);

		_bDataDirty = true;
		invalidateSeriesStyles();
	}

	/**
	 *  @private
	 */
	mx_internal function addElements(elements:Array, transform:DataTransform,
								   elementParent:UIComponent):void
	{
		var c:DisplayObject;
		var g:IChartElement;
		
		for (var i:int = 0; i < elements.length; i++)
		{
			c = elements[i];
			g = c as IChartElement;
			if (g)
				g.dataTransform = transform;

			elementParent.addChild(c);
		}
	}

	/**
	 * @private
	 */
	mx_internal function removeElements(parent:UIComponent,hasMask:Boolean):void
	{
		var n:int = parent.numChildren;
		var stopIndex:int = hasMask ? 1 : 0;
		for (var i:int = n - 1;
			 i >= stopIndex; // intentional -- leave the mask in place
			 i--)
		{
			var c:DisplayObject = parent.removeChildAt(i);
			var g:IChartElement = (c as IChartElement);
			if (g)
				g.dataTransform = null;
		}
	}
	
	/**
	 *  @private 
	 */
	mx_internal function updateSeriesStyles():void
	{
		if (_seriesStylesDirty)
		{
			_seriesStylesDirty = false;

			var nextAvailableStyle:uint = 0;
			
			var styles:Array = getStyle("chartSeriesStyles");
			if (styles)
			{
				var n:int = allElements.length;
				for (var i:int = 0; i < n; i++)
				{
					var g:IChartElement = allElements[i] as IChartElement;
					if (g)
					{
						nextAvailableStyle =
							g.claimStyles(styles, nextAvailableStyle);
					}
				}
			}
		}
	}

	/**
	 *  @private
	 */
	mx_internal function applyDataProvider(dp:ICollectionView,
										 transform:DataTransform):void
	{
		var axes:Object = transform.axes;
		for (var dimension:String in axes)
		{
			axes[dimension].chartDataProvider = dp;		
		}

		var elements:Array = transform.elements;
		for (var i:int = 0; i < elements.length; i++)
		{
			if (elements[i] is Series)
				elements[i].chartDataProvider = dp;
		}
	}

	/**
	 *  @private
	 */
	mx_internal function updateData():void
	{
		if (_dataProvider != null)
			applyDataProvider(_dataProvider,_transforms[0]);
	}

	/**
	 *  @private
	 */
	private function collectTransitions():Array
	{
		var a:Array = [];

		var n:int = allElements.length;
		for (var i:int = 0;i < n; i++)
		{
			var g:IChartElement = allElements[i] as IChartElement;
			if (g)
				g.collectTransitions(_transitionState, a);
		}
		
		return a;
	}

	//--------------------------------------------------------------------------
	//
	//  Event handlers
	//
	//--------------------------------------------------------------------------

	/**
	 *  @private
	 */
	private function mouseOverHandler(event:MouseEvent):void
	{
		if (_tipCache)
		{
			var n:int = _tipCache.count;
			for (var i:int = 0; i < n; i++)
			{
				var ti:IFlexDisplayObject = _tipCache.instances[i];
				
				if (event.target == ti ||
					((ti is DisplayObjectContainer) &&  
				   	 (ti as DisplayObjectContainer).contains(
						event.target as DisplayObject)))
				{				   
					return;
				}

				if (event.relatedObject != null &&
			 	 	(ti == event.relatedObject ||
					 ((ti is DisplayObjectContainer) && 
				 	  (ti as DisplayObjectContainer).contains(
						event.relatedObject))))
				{
					return;
				}
			}
		}
			
		if (event.relatedObject == null || 
			(event.relatedObject != this && 
			 !contains(event.relatedObject)))
		{
			var p:Point = new Point(event.localX, event.localY);
			p = globalToLocal(DisplayObject(event.target).localToGlobal(p));

			var hitPoints:Array = findDataPoints(p.x, p.y);
			
			processRollEvents(hitPoints, event);
		}
	}

	/**
	 *  @private
	 */
	private function mouseOutHandler(event:MouseEvent):void
	{

		if (_tipCache)
		{
			var n:int = _tipCache.count;
			for (var i:int = 0; i < n; i++)
			{
				var ti:IFlexDisplayObject = _tipCache.instances[i];

				if (event.target == ti ||
					((ti is DisplayObjectContainer) &&  
				   	 (ti as DisplayObjectContainer).contains(
						event.target as DisplayObject)))
				{				   
					return;
				}
				
				if (event.relatedObject != null &&
			 	 	(ti == event.relatedObject ||
					 ((ti is DisplayObjectContainer) && 
				 	  (ti as DisplayObjectContainer).contains(
							event.relatedObject))))
				{
					return;
				}
			}
		}

		if (event.relatedObject == null || 
			(event.relatedObject != this && 
			 !contains(event.relatedObject)))  
	   	{
			processRollEvents([], event);
		}
	}

	/**
	 *  @private
	 */
	private function mouseDownHandler(event:MouseEvent):void
	{
		var p:Point = new Point(event.localX, event.localY);
		p = globalToLocal(DisplayObject(event.target).localToGlobal(p));
		
		var hitPoints:Array = findDataPoints(p.x, p.y);

		processRollEvents(hitPoints, event);

		if (_currentHitSet != null)
		{
			dispatchEvent(new ChartItemEvent(ChartItemEvent.ITEM_MOUSE_DOWN,
											 _currentHitSet, event, this));
		}
	}

	/**
	 *  @private
	 */
	private function mouseMoveHandler(event:MouseEvent):void
	{
		if (_tipCache)
		{
			var n:int = _tipCache.count;
			for (var i:int = 0; i < n; i++)
			{
				var ti:IFlexDisplayObject = _tipCache.instances[i];

				if (event.target == ti ||
					((ti is DisplayObjectContainer) &&  
			   		(ti as DisplayObjectContainer).contains(
						event.target as DisplayObject)))
				{		   
					return;
				}
			}		
		}

		var p:Point = new Point(event.localX, event.localY);
		p = globalToLocal(DisplayObject(event.target).localToGlobal(p));
		
		var hitPoints:Array = findDataPoints(p.x, p.y);
		
		processRollEvents(hitPoints, event);
	}

	/**
	 *  @private
	 */
	private function mouseUpHandler(event:MouseEvent):void
	{
		var p:Point = new Point(event.localX, event.localY);
		p = globalToLocal(DisplayObject(event.target).localToGlobal(p));
		
		var hitPoints:Array = findDataPoints(p.x, p.y);

		processRollEvents(hitPoints, event);

		if (_currentHitSet != null)
		{
			dispatchEvent(new ChartItemEvent(ChartItemEvent.ITEM_MOUSE_UP,
											 _currentHitSet, event, this));
		}
	}

	/**
	 *  @private
	 */
	private function mouseClickHandler(event:MouseEvent):void
	{
		var p:Point = new Point(event.localX, event.localY);
		p = globalToLocal(DisplayObject(event.target).localToGlobal(p));

		var hitPoints:Array = findDataPoints(p.x, p.y);

		processRollEvents(hitPoints, event);

		if (_currentHitSet != null && _currentHitSet.length > 0)
		{
			dispatchEvent(new ChartItemEvent(ChartItemEvent.ITEM_CLICK,
											 _currentHitSet, event, this));
		}
	}

	/**
	 *  @private
	 */
	private function mouseDoubleClickHandler(event:MouseEvent):void
	{
		var p:Point = new Point(event.localX, event.localY);
		p = globalToLocal(DisplayObject(event.target).localToGlobal(p));
		
		var hitPoints:Array = findDataPoints(p.x, p.y);

		processRollEvents(hitPoints, event);

		if (_currentHitSet != null && _currentHitSet.length > 0)
		{
			dispatchEvent(new ChartItemEvent(ChartItemEvent.ITEM_DOUBLE_CLICK,
											 _currentHitSet, event, this));
		}
	}

	/**
	 *  @private
	 */
	private function dataEffectEndHandler(event:EffectEvent):void
	{
		event.target.removeEventListener(EffectEvent.EFFECT_END,
										 dataEffectEndHandler);

		if (chartState == ChartState.HIDING_DATA)
			setChartState(ChartState.PREPARING_TO_SHOW_DATA);
		else
			setChartState(ChartState.NONE);

		_transitionEffect = null;

		invalidateDisplayList();
	}
}

}

import mx.charts.HitData;
import mx.core.IFlexDisplayObject;

////////////////////////////////////////////////////////////////////////////////
//
//  Helper class: TipStack
//
////////////////////////////////////////////////////////////////////////////////

/**
 *  @private
 */
class TipStack
{
	//--------------------------------------------------------------------------
	//
	//  Constructor
	//
	//--------------------------------------------------------------------------

	/**
	 *  Constructor.
	 */
	public function TipStack()
	{
		super();
	}

	//--------------------------------------------------------------------------
	//
	//  Variables
	//
	//--------------------------------------------------------------------------

	/**
	 *  @private
	 */
	public var tips:Array = [];

	/**
	 *  @private
	 */
	public var height:Number;

	/**
	 *  @private
	 */
	public var gy:Number;

	//--------------------------------------------------------------------------
	//
	//  Methods
	//
	//--------------------------------------------------------------------------

	/**
	 *  @private
	 */
	public function merge(ts:TipStack, screenHeight:Number):void
	{
		tips = tips.concat(ts.tips);

		var newHeight:Number = ts.gy + ts.height - gy;
		gy = Math.max(0, gy - (gy + height - ts.gy) / 2);
		if (gy + newHeight > screenHeight)
			gy = Math.max(0, screenHeight - newHeight);
		height = newHeight;
	}

	/**
	 *  @private
	 */
	public function addTip(tip:TipPositionData, screenHeight:Number):void
	{
		tips.push(tip);

		if (tips.length == 1)
		{
			gy = tip.py;
			height = tip.height;
		}
		else if (tips.length == 2)
		{
			height += tip.height;
			gy = Math.min(screenHeight - height,
				 Math.max(0, (tips[0].gy + tips[1].gy) / 2) - height / 2);
		}
		else
		{
			height += tip.height;
			gy = Math.min(screenHeight - height,
				 Math.max(0, ((gy + height / 2) *
				 (tips.length - 1) + tip.gy) / tips.length - height / 2));
		}
	}

	/**
	 *  @private
	 */
	public function positionY():void
	{
		var gy:Number = this.gy;
		for (var i:int = 0; i < tips.length; i++)
		{
			var tipData:TipPositionData = tips[i];
			tipData.py = gy;
			gy += tipData.height;
		}
	}
}

////////////////////////////////////////////////////////////////////////////////
//
//  Helper class: TipPositionData
//
////////////////////////////////////////////////////////////////////////////////

/**
 *  @private
 */
class TipPositionData
{
	//--------------------------------------------------------------------------
	//
	//  Constructor
	//
	//--------------------------------------------------------------------------

	/**
	 *  Constructor.
	 */
	public function TipPositionData(tipInstance:IFlexDisplayObject,
									hitData:HitData,
									x:Number, y:Number,
									gx:Number, gy:Number)
	{
		super();

		tip = tipInstance;
		hd = hitData;
		this.x = x;
		this.y = y;
		this.gx = gx;
		this.gy = gy;
		this.width = tipInstance.measuredWidth;
		this.height = tipInstance.measuredHeight+4;
	}

	//--------------------------------------------------------------------------
	//
	//  Variables
	//
	//--------------------------------------------------------------------------

	/**
	 *  @private
	 */
	public var tip:IFlexDisplayObject;

	/**
	 *  @private
	 */
	public var hd:HitData;

	/**
	 *  @private
	 */
	public var x:Number;

	/**
	 *  @private
	 */
	public var y:Number;

	/**
	 *  @private
	 */
	public var gx:Number;

	/**
	 *  @private
	 */
	public var gy:Number;

	/**
	 *  @private
	 */
	public var px:Number;

	/**
	 *  @private
	 */
	public var py:Number;

	/**
	 *  @private
	 */
	public var width:Number;

	/**
	 *  @private
	 */
	public var height:Number;

	/**
	 *  @private
	 */
	public var isRight:Boolean;
}
